/**
 * @file videocapturer.cpp
 * @author your name (you@domain.com)
 * @brief
 * @version 0.1
 * @date 2022-10-20
 *
 * @copyright Copyright (c) 2022
 *
 */

#include "src/camera/videocapturer.h"

#include <QCoreApplication>
#include <QDebug>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

static const char *input_format_name = "video4linux2";

VideoCapturer::VideoCapturer(QObject *parent, bool convert2rgb)
    : QThread{parent}, m_bConvert2Rgb(convert2rgb) {
  avdevice_register_all();
}

VideoCapturer::~VideoCapturer() {
  this->m_bRunning.store(false);
  wait();
}

bool VideoCapturer::open(const QString &device_name) {
  if (!this->isFinished()) {
    close();
  }

  auto fmt = av_find_input_format(input_format_name);
  if (nullptr == fmt) {
    return false;
  }

  m_pCtx = avformat_alloc_context();
  AVDictionary *options = nullptr;
  av_dict_set(&options, "framerate", "60", 0);
  // av_dict_set(&options, "input_format", "mjpeg", 0);
  av_dict_set(&options, "pixel_format", "mjpeg", 0);
  int ret = avformat_open_input(&m_pCtx, device_name.toStdString().data(), fmt,
                                &options);
  av_dict_free(&options);

  if (0 != ret) {
    clear();
    return false;
  }

  // 打开解码器
  if (!openDecoder()) {
    return false;
  }

  m_bRunning.store(true);
  this->start();

  return true;
}

void VideoCapturer::close() {
  m_bRunning.store(false);
  wait();
}

void VideoCapturer::run() {
  while (m_bRunning.load()) {
    read();
  }

  // 线程结束时清理资源
  clear();
}

// 打开解码器
bool VideoCapturer::openDecoder() {
  AVCodec *decoder = nullptr;
  av_find_best_stream(m_pCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder, 0);
  if (nullptr == decoder) {
    return false;
  }

  m_pDecoderCtx = avcodec_alloc_context3(decoder);
  auto ret = avcodec_open2(m_pDecoderCtx, decoder, nullptr);
  if (0 != ret) {
    avcodec_free_context(&m_pDecoderCtx);
    m_pDecoderCtx = nullptr;
    return false;
  }

  return true;
}

void VideoCapturer::read() {
  AVPacket pkt;

  int ret = av_read_frame(m_pCtx, &pkt);
  if (ret < 0) {
    emit error("Error during reading frame.");
    return;
  }

  ret = avcodec_send_packet(m_pDecoderCtx, &pkt);
  av_packet_unref(&pkt);

  if (ret < 0) {
    emit error("Error sending a packet for decoding.");
    return;
  }

  while (ret >= 0) {
    auto frame = av_frame_alloc();
    ret = avcodec_receive_frame(m_pDecoderCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
      av_frame_free(&frame);  // 注意，经常会执行到这里，不要忘记释放内存
      return;
    } else if (ret < 0) {
      av_frame_free(&frame);
      emit error("Error during decoding.");
      return;
    }

    if (m_bConvert2Rgb) {
      AVFrame *rgbFrame = nullptr;
      if (yuv2rgb(frame, &rgbFrame)) {
        // 构造并且发送AVFrame的只能智能指针 RGB
        auto spFrame = std::shared_ptr<AVFrame>(
            rgbFrame, [](AVFrame *frame) { av_frame_free(&frame); });
        emit appending_frame(spFrame);
      } else {
        // 转换出错 TODO
      }
      av_frame_free(&frame);
    } else {
      // 构造并且发送AVFrame的只能智能指针 YUV
      emit appending_frame(std::shared_ptr<AVFrame>(
          frame, [](AVFrame *frame) { av_frame_free(&frame); }));
    }
  }
}

void VideoCapturer::clear() {
  if (m_pCtx) {
    avformat_close_input(&m_pCtx);
  }

  if (m_pDecoderCtx) {
    avcodec_free_context(&m_pDecoderCtx);
  }

  if (m_pConvert) {
    sws_freeContext(m_pConvert);
    m_pConvert = nullptr;
  }
}

bool VideoCapturer::yuv2rgb(AVFrame *in, AVFrame **out) {
  *out = av_frame_alloc();
  (*out)->format = AVPixelFormat::AV_PIX_FMT_RGB24;
  (*out)->width = in->width;
  (*out)->height = in->height;
  av_frame_get_buffer(*out, 1);

  if (nullptr == m_pConvert) {
    m_pConvert = sws_getContext(in->width, in->height,
                                AVPixelFormat(in->format), (*out)->width,
                                (*out)->height, AVPixelFormat::AV_PIX_FMT_RGB24,
                                0, nullptr, nullptr, nullptr);
  }

  int ret = sws_scale(m_pConvert, in->data, in->linesize, 0, in->height,
                      (*out)->data, (*out)->linesize);
  return (*out)->height == ret;
}
